#------------------------------------
# pxbdb.py -Python XML Bean Server
# 
# david connell
# 03/15/2005
#------------------------------------
"""
This class was extracted from the pxbcore
module and enhanced for prime time deployments.

Enhancements include swapping out the logging 
mechanism for the standard Python logging module
and the addition of a 'lazy write' function for 
persisting the pxb's held in the server.
"""

import sys, os, logging
try:
	import hashlib
except:
	import md5
	 
import threading
import time
from datetime import datetime
now = datetime.now


from pxbcore2 import pxb

#
# -- Server maintenance thread
#
class pxbServiceAgent(threading.Thread):
	def __init__(self, f, p, **kwds):
		' set up the thread processing'
		threading.Thread.__init__(self, **kwds)
		self._method = f
		self._period = p
		self._go = 1
		
	def run(self):
		'run the thread'
		self._go = 1
		while self._go:
			self._method()
			time.sleep(self._period)
			
	def halt(self):
		'stop the thread'
		self._go = 0
	
#
# -- PB Server Class
#
class pxbServer(object):
	"""Manager class for PXB housing and sharing"""

	#----------------------------------------------------
	# Construction
	#----------------------------------------------------
	def __init__(self, dir=".", cfgfile="pxb.xml"):
		"""setup server internals"""
		self._pxbs = {}
		self._home = dir
		self._loaded = now()
		
		# set default sync cycle to 15 secs
		self._syncRate = 15;
	
		# if config exists, use it
		try:
			# ceate the config pxb
			self._cfg = pxb()
			self._cfg.load(cfgfile)

			# process configuration
			self._configure()
		
			# save config
			self._pxbs['pxbsvrcfg'] = { "name":"pxbsvrcfg", "handle":'pxbsvrcfg',
							  "filename":cfgfile, "pxb":self._cfg,
							  "count": 0, "load":self._loaded, "dirty": 0,
							  "sync": self._loaded, "syncCount":0 }
							  
			# start up the maintenance agents
			self._start()
			
		except:
			# no configuration data
			self._cfg = None
			self._pxbs['pxbsvrcfg'] = { "name":"pxbsvrcfgs", "handle":'pxbsvrcfg',
							  "filename":'', "pxb":None, "dirty": 0,
							  "count": 0, "load":self._loaded, "sync": self._loaded,
							  "syncCount":0  }
							  
			logging.getLogger().exception('error loading configuration')
							  
		
	#----------------------------------------------------
	# Internal methods			
	#----------------------------------------------------
	def _start(self):
		'Start up any / all server agents'
		# start the sync agent daemon
		self._syncAgent = pxbServiceAgent(self._stateCheck, self._syncRate)
		self._syncAgent.setDaemon(True)
		self._syncAgent.start()
		self.logInfoMsg("Sync Agent started")
		
	def _configure(self):
		"""read and process the configuration file"""
		pxbCfg = self._cfg
		log = 'stdout'
		logLevel = 0 
		conflog = []
		
		try:
			# no config is a valid config choice
			if pxbCfg != None:
				# look for root directory overrides
				d = pxbCfg.getNodeValue("//server/root")	
				if d:
					self._home = d
					
				conflog.append("Server Root moved to %s" % self._home)

				# set the logfile name and logging level
				log = 'pxbserver.log'
				l = pxbCfg.getNodeValue("//server/logging/logfile")
				if l:
					log = l
				
				if log[0] in  '/.':
					# preset location
					logfile = log
				else:
					# relative to root
					logfile = "%s%s%s" % (self._home, os.sep, log)
				
				conflog.append("Logging to %s" % logfile)
				
				# set the default logging level
				dl = pxbCfg.getNodeValue("//server/logging/deflevel")
				if dl:
					logLevel = dl.upper()
				else:
					# defaul log level = INFO
					logLevel = 'INFO'  			
					
				# and save it	
				self._logLevel = logLevel					
				
				conflog.append('Default logging level set at %s' % logLevel)
			
				# set the logging threshold (verbosity)
				tl = pxbCfg.getNodeValue("//server/logging/threshold")
				if dl:
					threshold = tl.upper()
				else:
					# defaul log level = INFO
					threshold = 'INFO'  								

				conflog.append('Logging Threshold set at %s' % threshold)
					
				# we have enough to set up logging							
				self._setupLogging(logfile, threshold)
									
				# start logging now...
				self.logInfoMsg("Starting pxbServer...")

				# write out saved conflogs
				for msg in conflog:
					self.logInfoMsg("[conflog]:%s" % msg)
				
				self.logInfoMsg("...pxbServer is up and running.")
				
			
				# check for sync rate override
				sr = pxbCfg.getNodeValue("//server/syncrate")
				if sr:
					self._syncRate = int(sr)

				self.logInfoMsg("Sync Rate set to %d" % self._syncRate)
															
				# get the preloads listing from pxbQuery
				q1 = pxbCfg.createQuery("//startup/preload")
				r1 = q1.execute()
	
				# load each file in list
				while r1.hasNext():
					# get the next pxb 
					p = r1.getNext()
					fn = p.getNodeValue('./filename')
					nm = p.getNodeValue('./pxbname')
	
					# load up the PXB into the server
					hndl, nm, node = self.load(fn, nm)
				
					conflog.append("File %s preloaded" % nm)
				
			
		except:
			logging.exception('error loading configuration')
			print "Conf Log Dump"
			for l in conflog:
				print l


	def _stateCheck(self):			
		'method to inspect the entries and do house cleaning, purging, etc'
		for handle, pn in self._pxbs.items():
			if pn['dirty']:
				logging.info('Saving %s (%s)' % (pn['name'], pn['filename']))
				self._updateEntry(pn)
			
		# self.logDebugMsg("StateCheck")
					
	def _updateEntry(self, entry):
		'''Save the state if an entry to disk'''

		self.logInfoMsg('Saving %s (%s) to disk' % (entry['name'], entry['filename']))

		entry['pxb'].save()
		entry['sync'] = now()
		entry['syncCount'] += 1
		entry['dirty'] = 0
		

	def _setupLogging(self, logFile='./pxbsvr.log', level='INFO'):
		'''Prepare things for the logging system'''
		# set up log level and formatting
		lvl = eval('logging.%s' % level.upper())
		fmtr = logging.Formatter('[%(asctime)s] PXBServer <%(levelname)s>: %(message)s')

		# set up console logging
		console = logging.StreamHandler(sys.stdout)
		console.setFormatter(fmtr)
		logging.getLogger('').addHandler(console)
		
		# set up file logging
		flog = logging.FileHandler(logFile)
		flog.setFormatter(fmtr)
		logging.getLogger('').addHandler(flog)
		
		# create internal logger
		self._logger = logging.getLogger('pxbServer')
		self._logger.setLevel(lvl)
						
	#--------------------------------
	# logging api
	#--------------------------------
	def logMsg(self, msg, level=None):
		'''dump message to logs'''
		# determine log level
		lvl = level
		if level is None:
			lvl = self._logLevel
			
		# find method for level
		logMethod = eval('self._logger.%s' % lvl.lower())

		# log message
		logMethod(msg)

	def logDebugMsg(self, msg):
		self.logMsg(msg, 'debug')
		
	def logInfoMsg(self, msg):
		self.logMsg(msg, 'info')

	def logWarnMsg(self, msg):
		self.logMsg(msg, 'warning')

	def logErrorMsg(self, msg):
		self.logMsg(msg, 'error')

	def logFatalMsg(self, msg):
		self.logMsg(msg, 'critical')

	def logExceptionMsg(self, msg):
		self.logMsg(msg, 'exception')
		
	#----------------------------------------------------
	# Public API
	#----------------------------------------------------
	def getPXBDir(self):
		'''return a list of 'entries' describing the contents'''
		rv = []
		for key, pn in self._pxbs.items():
			entry = {}
			entry['Name'] = pn['name']
			entry['File'] = pn['filename']
			entry['Handle'] = pn['handle']
			entry['RefCount'] = pn['count']
			entry['Load'] = pn['load']
			entry["Sync"] = pn['sync']
			entry['Updates'] = pn['syncCount']
			entry['Dirty'] = pn['dirty']
				
			rv.append(entry)
			
		return rv
		
	def getPXBNames(self):
		"""return list of filenames"""
		rv = []
		for key in self._pxbs.keys():
			rv.append(self._pxbs[key]["name"])

		return rv	
	
	def getPXB(self, handle):
		"""return the PXB stored at handle"""
		rv = None
		if self._pxbs.has_key(handle):
			rv = self._pxbs[handle]

		return rv
					
	def getPXBsByName(self, name):
		"""retrieve a list of PXBs by name"""
		rv = []
		
		for key in self._pxbs.keys():
			"""walk them all"""
			if self._pxbs[key]["name"] == name:
				rv.append(self._pxbs[key]["pxb"])
				
		return rv
		
	def getPXBHandle(self, name):
		'Look up pxb by name and return handle'
		rv = -1
		
		# look it up in the directory
		for d in self.getPXBDir():
			if d['Name'] == name:
				rv = d['Handle']
				break;
				
		return rv				
		
	def syncPXB(self, handle):
		'Save the shared resource to disk and update entry'
		rv = 0
		
		if self._pxbs.has_key(handle):
			pn = self._pxbs[handle]
			if self._syncRate:
				# set dirty flag
				pn['dirty']
				
			else:			
				# take care of it now!
				self._updateEntry(pn)

			pn['syncCount'] += 1
					
			self.logInfoMsg('Sync request for %s (%s) [syncCount = %d]' % (pn['name'], pn['filename'], pn['syncCount']))
		else:
			self.logWarnMsg('Sync requested for invalid handle %s' % handle)
								
	def unload(self, handle):
		"""unload the pxb"""
		if self._pxbs.has_key(handle):
			pn = self._pxbs[handle]

			# dec ref count
			pn["count"] -= 1
			
			# if done, loose it (unless it's the config)
			if pn["count"] < 1 and pn['name'] != 'conf':
				self._pxbs[handle] = None 
				self._pxbs.__delitem__(handle)
				
		self.logInfoMsg('PXB %s (%s) unloaded [count=%d]' % (pn['name'], pn['filename'], pn['count']))
						
	def load(self, filename, name=""):	
		"""load a file, if needed, and return a tuple 
		containing the handle,the name and the pxb"""
		# work vars
		target = ""
		p = None
		handle = -1
		pn = {}
		rv = None
		
		# resolve path
		if filename[0] == os.sep:
			# absolute file name
			target = filename
			
		else:
			# relative path
			target = "%s/%s" % (self._home, filename)
		
		
		# determine the key
		md = None
		try:
			md = hashlib.md5()
		except:
			md = md5.new()
			
		md.update(target)
		handle = md.digest()

		# check cache
		if self._pxbs.has_key(handle):
			pn = self._pxbs[handle]

		else:	
			# build the pxb and node from scratch
			p = pxb()
			p.load(target)
		
			# get the name
			if name == "":
				"""use filename minus ext"""
				name = os.path.splitext(os.path.split(target)[1])[0]

			# build node
			dt = now()
			pn = {"name":name, "filename":target, "pxb":p, "handle":handle,
				  "count":0, "load":dt, "dirty":0, "sync":dt, "syncCount":0 }
			
			# save the node 
			self._pxbs[handle] = pn

		# bump references
		pn["count"] += 1

		self.logInfoMsg('PXB %s (%s) loaded [count=%d]' % (pn['name'], pn['filename'], pn['count']))
				
		# return pertinent data
		return (pn["handle"], pn["name"], pn["pxb"])	
